﻿/**
 * Copyright (c) 2009 Rtistique Pty. Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.rtistique.bzoo {
	
	import com.adobe.serialization.json.JSON;
	import flash.net.SharedObject;
	import org.as3yaml.YAML;
	
	/**
	 * @author 	Bachir El Khoury
	 * @date 	13/2/2009
	 * @version 0.1.3
	 * - Added persistent data support
	 * - Made find feature non case sensitive
	 * @version
	 * 0.1.2
	 * - Update method now supports array of fields to udpate
	 * @version
	 * 0.1.0: 
	 * - Beta Release
	 * 
	 * @description
	 * Bzoo is an AS3 library that provides a thin data layer 
	 * for flash/flex applications or websites, allowing data 
	 * manipulation.<br/>
	 * It works like a temporary database, adapting the CRUD 
	 * interface:
	 * <ul>
	 * <li>Create</li>
	 * <li>Read</li>
	 * <li>Update</li>
	 * <li>Delete</li>
	 * </ul>
	 * <br/>
	 * 
	 * @usage 
	 * <pre><code>import com.rtistique.bzoo.Bzoo;
	 * var friends:Bzoo = new Bzoo(data);</code></pre>
	 * 
	 * check the working example at:
	 * http://rtistique.com/blog/?page_id=23
	 * 
	 */
	
	public class Bzoo 
	{
		/**
		 * Static string constant "all"
		 */
		public static const FIND_ALL	:String = "all";
		/**
		 * Static string constant "first"
		 */
		public static const FIND_FIRST	:String = "first";
		/**
		 * Static string constant "count"
		 */
		public static const FIND_COUNT	:String	= "count";
		/**
		 * Static string constant "ascending"
		 */
		public static const ASCENDING	:String	= "ascending";
		/**
		 * Static string constant "descending"
		 */
		public static const DESCENDING	:String	= "descending";
		/**
		 * Static string constant "numeric"
		 */
		public static const NUMERIC		:String	= "numeric";
		
		/**
		 * Data type static string constant "amf"
		 */
		public static const TYPE_AMF	:String = "amf";
		/**
		 * Data type static string constant "yaml"
		 */
		public static const TYPE_YAML	:String = "yaml";
		/**
		 * Data type static string constant "json"
		 */
		public static const TYPE_JSON	:String = "json";
		
		private const GREATER_THAN		:String = "greaterThan";
		private const LESS_THAN			:String	= "lessThan";
		private const EQUAL				:String	= "equal";
		
		private var _data				:Object;
		private var _so					:SharedObject;
		private var _conditionFields	:Array;
		private var _conditionValues	:Array;
		private var _comparisonValues	:Array;
		private var _conditionField		:String;
		private var _conditionValue		:String;
		private var _fieldFilter		:String;
		private var _comparisonValue	:String;
		private var _isDeleting			:Boolean;
		private var _isPersistent		:Boolean;
		private var _name				:String;
		
		/**
		 * Data collection
		 */
		public function get data():Array { return _data as Array; }
		
		public function set data(value:Array):void 
		{
			_data = value;
		}
		/**
		 * Set data persistence
		 */
		public function get isPersistent():Boolean { return _isPersistent; }
		
		public function set isPersistent(value:Boolean):void 
		{
			_isPersistent 		= value;
			_so.data.persistent	= _isPersistent;
		}
		/**
		 * The Bzoo constructor method takes data object and data type string as parameters
		 * @param	data Data object that can either be an array of similar objects, a JSON string or a YAML string
		 * @param	type The data type. Default "amf". Other values can be "json" or "yaml"
		 * @example
		 * <pre>
	 * var data:Array = [
	{name:"Bob",
	  gender:"M",
	  married:"No",
	  age:25,
	  state:"NY",
	  favorite_foods:["pizza","tacos"]},
	{name:"Joyce",
	  gender:"F",
	  married:"No",
	  age:29,
	  state:"WA",
	  favorite_foods:["salad","cheese sticks"]},
	{name:"Dan",
	  gender:"M",
	  married:"No",
	  age:29,
	  state:"MT",
	  favorite_foods:["pizza","hamburgers","BLTs"]},
	{name:"Sarah",
	  gender:"F",
	  married:"No",
	  age:21,
	  state:"ID",
	  favorite_foods:["pizza","sushi"]}
	  ];
		 * <br/><b>var friends:Bzoo = new Bzoo(data);</b>
		 */
		public function Bzoo(data:Object, type:String = TYPE_AMF, isPersistent:Boolean = false, name:String = "bzoo") 
		{
			_isPersistent 	= isPersistent;
			_name			= name;
			
			switch (type) 
			{
				case TYPE_AMF:
					_data = data;
				break;
				case TYPE_YAML:
					_data = YAML.decode(data.toString());
				break;
				case TYPE_JSON:
					_data = JSON.decode(data.toString());
				break;
			}
			setPersistence();
			checkPersistance();
		}
		/**
		 * The find method takes type, conditions, fields and orderBy params and returns filtered array results
		 * @param	type String. "all", "first", "count"
		 * @param	params Object holding conditions, fields and orderBy params
		 * @return  filtered Array result
		 * @example 
		 * <pre>
		 * <br/>
		 * // Example 1: 
		 * // Finding all people that are 29 years old
		 * var result:Object = bzoo.find("all", { 
		 * 				conditions	:[{ field:"age", equal:"29"}]);
		 * <br/>
		 * 
		 * // Example 2: 
		 * // The result will be an object with only the name and the state attributes, where person's age is greater than 21
		 * // sort the result by state Ascending
		 * var result:Object = bzoo.find("all", { 
		 * 				fields		:["name", "state"], 
		 * 				conditions	:[{ field:"age", greaterThan:"21"}], 
		 * 				orderBy		:{ field:"state", order:Bzoo.ASCENDING} } ); 
		 * <br/>
		 * // Example 3: 
		 * // Finding the first person that is 29 years old
		 * var result:Object = bzoo.find("first", { 
		 * 				conditions	:[{ field:"age", greaterThan:"21"}]);
		 * <br/>
		 * </pre>
		 */
		public function find(type:String = FIND_ALL, params:Object = null):Object
		{
			var result:Object = findAll(params);
			
			switch (type) 
			{
				case FIND_ALL:					
					return result;
				break;
				case FIND_FIRST:
					return result[0];
				break;
				case FIND_COUNT:
					return result.length;
				break;
			}
			return null;
		}
		/**
		 * Insert a new data object or an array of objects
		 * @param	data Object or array to be inserted
		 * @example
		 * <pre>
		 * <br/>
		 * bzoo.insert( { name:"Bachir", 
		 * 		gender:"M", 
		 * 		married:"YES", 
		 * 		age: 28, 
		 * 		state:"NSW", 
		 * 		favorite_food:["Moussaka", "Akroshka"] });
		 * <br/>
		 * </pre>
		 */
		public function insert(data:Object):void
		{
			var result:Array = [];
			result = _data.concat();
			if (data.length == undefined)
			{
				result.push(data);
			}
			else
			{
				for each (var item:Object in data) 
				{
					result.push(item);
				}
			}
			_data = result;
			flushData();
		}
		/**
		 * Delete object that matches criteria
		 * @param	params <tt>Object</tt> Holding conditions, fields and orderBy params
		 * @return	filtered Array result
		 */
		public function remove(params:Object):Array
		{
			_isDeleting 		= true;
			var result:Array 	= setConditions(params.conditions);
			data 				= result;
			flushData();
			return data;
		}
		/**
		 * Delete by index object form data 
		 * @param	index <tt>Number</tt> Index of the object to be deleted
		 * @return 	udpated array after deletion
		 */
		public function removeByIndex(index:Number):Array
		{
			_data.splice(index, 1);
			flushData();
			return _data as Array;
		}
		/**
		 * Update objects that matches criteria
		 * @param	params <tt>Object</tt> Holding conditions, fields and orderBy params
		 * @param	values <tt>Object</tt> New object values to be set for the object
		 * @return	udpated array after update
		 */
		public function update(params:Object, values:Object):Array
		{
			var result:Object = findAll(params);
			if (values.length == undefined)
			{
				for each (var item:Object in result) 
				{
					item[values.field] = values.equal;
				}
			}
			else {
				for each (item in result) 
				{
					for each (var value:Object in values)
					{
						item[value.field] = value.equal;
					}
				}
			}
			flushData();
			return data;
		}
		/**
		 * Sort data array
		 * @param	orderBy <tt>String</tt> field name the array will be sorted by
		 * @param	orderType <tt>String</tt> The sorting type "ascending", "descending", "numeric"
		 * @return	sorted array
		 */
		public function orderBy(orderBy:String, orderType:String):Array
		{
			var result:Array  = order(data, orderBy, orderType);
			return result;
		}
		/**
		 * Clear persistent data from SO
		 */
		public function clearData():void
		{
			_so.clear();
		}
		/**
		 * Check data persistence. If true data is retrieved from Shared Object.
		 */
		private function checkPersistance():void
		{
			if (_isPersistent)
			{
				if (_so.data.data == null)
				{
					flushData();
				}else {
					
					_data = _so.data.data;
					return;
				}
				_so.data.persistent = _isPersistent;
			}
		}
		/*
		 * Find all results
		 * @param	params <tt>Object</tt> Holding conditions, fields and orderBy params
		 * @return  result Array
		 */
		private function findAll(params:Object):Object
		{
			var result:Array 	= [];
			result 				= _data.concat();
			_isDeleting 		= false;
			
			if (params != null)
			{
				result = setConditions(params.conditions);
				result = setFieldsFilter(result, params.fields);
				result = setOrder(result, params.orderBy);
			}
			return result;
		}
		/**
		 * Save data to Shared Object
		 */
		private function flushData():void
		{
			if (_isPersistent)
			{
				_so.data.data = _data;
				_so.flush();
			}
		}
		/**
		 * Set data persistence
		 */
		private function setPersistence():void
		{
			_so = SharedObject.getLocal(_name);
			
			if (_so.data.persistent == null)
			{
				_so.data.persistent = _isPersistent;
			}
			else 
			{
				_isPersistent = _so.data.persistent;
			}
		}
		/*
		 * Set the fields filter
		 * @param	data Array of objects
		 * @param	fields Array of string fields
		 * @return 	udpated data Array
		 */
		private function setFieldsFilter(data:Array, fields:Array):Array
		{
			var result:Array = data.concat();
			if (fields != null)
			{
				var filteredResult:Array = [];
				for each( var item:Object in data)
				{
					var obj:Object = {};
					for each (var field:String in fields)
					{
						obj[field] = item[field];
					}
					filteredResult.push(obj);
				}
				result = filteredResult;
			}
			return result;
		}
		/*
		 * Set order
		 * @param	data Array of objects
		 * @param	orderBy Object 
		 * @return 	updated data Array 
		 */
		private function setOrder(data:Array, orderBy:Object):Array
		{
			var result:Array = data.concat();
			if (orderBy != null)
			{
				result = order(result, orderBy.field, orderBy.order);
			}
			return result;
		}
		/*
		 * Set conditions
		 * @param	data Array of objects
		 * @param	conditions Object
		 * @return udpate data Array
		 */
		private function setConditions(conditions:Object):Array
		{
			var result:Array = data.concat();
			if (conditions != null)
			{
				_conditionFields = [];
				_conditionValues = [];
				_comparisonValues = [];
				
				if (conditions.length == undefined)
				{
					setConditionValues(conditions);	
				}
				else {
					for each (var condition:Object in conditions) 
					{
						setConditionValues(condition);
					}
				}
				
				for (var i:int = 0; i < _conditionFields.length; i++) 
				{
					_conditionField = _conditionFields[i];
					_conditionValue = _conditionValues[i];
					_comparisonValue = _comparisonValues[i];
					result = (!_isDeleting) ? result.filter(conditionsFilter).concat()
											: result.filter(deleteFilter).concat();
				}
			}
			return result;
		}
		/*
		 * Sets the condition field, value and comparison
		 * @param	condition Object
		 */
		private function setConditionValues(condition:Object):void
		{
			var cnd:String = 	(condition.greaterThan 	!= undefined) ? GREATER_THAN 	:
								(condition.lessThan 	!= undefined) ? LESS_THAN 		:
								(condition.equal 		!= undefined) ? EQUAL : null;
								
			_conditionFields.push(condition.field);
			_conditionValues.push(condition[cnd]);
			_comparisonValues.push(cnd);
		}
		/*
		 * Order data array
		 * @param	array Array of objects
		 * @param	orderBy
		 * @param	orderType
		 * @return 	ordered array
		 */
		private function order(array:Array, orderBy:String, orderType:String):Array
		{
			switch (orderType) 
			{
				case ASCENDING:
					array.sortOn(orderBy);
					return array;
				break;
				case DESCENDING:
					array.sortOn(orderBy, Array.DESCENDING);
					return array;
				break;
				case NUMERIC:
					array.sortOn(orderBy, Array.NUMERIC);
					return array;
				break;
			}
			return null;
		}
		/*
		 * Delete filter
		 * @param	item
		 * @param	index
		 * @param	array
		 * @return
		 */
		private function deleteFilter(item:*, index:int, array:Array):Boolean
		{
			switch (_comparisonValue) 
			{
				case GREATER_THAN:
					return (item[_conditionField] < Number(_conditionValue));
				break;
				case LESS_THAN:
					return (item[_conditionField] > Number(_conditionValue));
				break;
				case EQUAL:
					return (item[_conditionField] != _conditionValue);
				break;
			}
			return false;
		}
		/*
		 * Conditions filter
		 * @param	item
		 * @param	index
		 * @param	array
		 * @return
		 */
		private function conditionsFilter(item:*, index:int, array:Array):Boolean
		{ 
			switch (_comparisonValue) 
			{
				case GREATER_THAN:
					return (item[_conditionField] > Number(_conditionValue));
				break;
				case LESS_THAN:
					return (item[_conditionField] < Number(_conditionValue));
				break;
				case EQUAL:
					return (item[_conditionField].toString().toLowerCase() == _conditionValue.toString().toLowerCase());
				break;
			}
			return false;
		}
	}
}